package com.easyselection.utils;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

@Slf4j
@Component
public class CacheClient {
    private final StringRedisTemplate stringRedisTemplate;

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public void set(String key, Object value, Long time, TimeUnit unit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }

    //设置逻辑过期时间
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));

        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    //缓存空值解决缓存穿透问题

    /**
     *
     * @param cacheKeyPrefix  缓存key的前缀
     * @param id 主键id值
     * @param type 操作的数据类型
     * @param dbFallBack 数据库后备 查询数据库
     * @param time 时间
     * @param unit 时间单位
     * @return 返回结果
     * @param <T> 数据参数和返回值参数
     * @param <ID> id类型
     */
    public  <T, ID> T queryWithPassThrough(String cacheKeyPrefix, ID id, Class<T> type,
                                           Function<ID, T> dbFallBack, Long time, TimeUnit unit){
        //1.从redis当中查询商铺缓存
        String key = cacheKeyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);
        //2.判断是否存在
        if(StrUtil.isNotBlank(json)){
            //3.存在, 返回信息
            return JSONUtil.toBean(json, type);
        }
        //判断命中的是否是空字符串, 如果是则返回空信息
        if(json != null){
            return null;
        }

        //4.不存在 根据id查询数据库
        T result = dbFallBack.apply(id);
        //5.数据库中不存在, 为了解决缓存穿透, 将空值写入redis, 并返回错误信息
        if(result == null) {
            stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            //返回错误信息
            return null;
        }
        //6、存在 写入redis
        this.set(key, result, time, unit);
        //返回信息
        return result;
    }

    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);


    /**
     * 基于逻辑过期方案解决缓存击穿问题
     * @param cacheKeyPrefix 缓存key的前缀
     * @param lockKeyPrefix 互斥锁key的前缀
     * @param id 主键id值
     * @param type 操作的数据类型
     * @param dbFallBack 数据库后备 查询数据库
     * @param time 时间
     * @param unit 时间单位
     * @return 返回结果
     * @param <T> 数据参数和返回值参数
     * @param <ID> id类型
     */
    public <T, ID> T queryWithLogicalExpire(String cacheKeyPrefix, String lockKeyPrefix, ID id, Class<T> type,
                                             Function<ID, T> dbFallBack, Long time, TimeUnit unit){
        //1.从redis当中查询商铺缓存
        String key = cacheKeyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);
        //2.判断是否存在
        if(StrUtil.isBlank(json)) {
            //3.未命中返回空
            return null;
        }

        //4.判断缓存是否过期
        //反序列化 将Json数据转换为redisData对象
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        //将data Json字符串也反序列化为shop对象
        JSONObject JsonObject = (JSONObject) redisData.getData();
        T result = JSONUtil.toBean(JsonObject, type);
        LocalDateTime expireTime = redisData.getExpireTime();
        //5. 判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())){
            //5.1 未过期返回商铺信息
            return result;
        }

        //5.2 已过期 需要进行缓存重建
        //6. 缓存重建
        //6.1 获取互斥锁
        String lockKey = lockKeyPrefix + id;
        boolean isLock = tryLock(lockKey);
        //6.2 获取锁成功 开启独立线程
        if(isLock){

            //双重检验锁机制: 再次检查缓存是否过期（防止多个线程同时重建缓存）
            String currentShopJson = stringRedisTemplate.opsForValue().get(key);
            //反序列化
            RedisData currentRedisData = JSONUtil.toBean(currentShopJson, RedisData.class);
            if (currentRedisData.getExpireTime().isAfter(LocalDateTime.now())) {
                unLock(lockKey);
                return JSONUtil.toBean((JSONObject) currentRedisData.getData(),type);
            }

            // 提交独立线程进行缓存重建
            CACHE_REBUILD_EXECUTOR.submit(()->{
                try {
                    //重建缓存
                    //1.查询数据库
                    T result1 = dbFallBack.apply(id);
                    //2.封装逻辑过期时间
                    this.setWithLogicalExpire(key, result1, time, unit);

                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    //释放锁
                    unLock(lockKey);
                }
            });
        }

        //6.3 返回旧的缓存信息
        return result;
    }

    /**
     * 获取锁
     * @param key
     * @return
     */
    private boolean tryLock(String key){
        return Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(key, "1",
                RedisConstants.LOCK_SHOP_TTL, TimeUnit.MINUTES));
    }

    /**
     * 释放锁
     * @param key
     */
    private void unLock(String key){
        stringRedisTemplate.delete(key);
    }
}
